<?php

Raise::load('core.RaiseObject');
Raise::load('core.RaiseApplication');
Raise::load('core.RaiseCollection');
Raise::load('core.RaiseTimeSpan');
Raise::load('core.Url');
Raise::load('core.RaiseVariable');
Raise::load('control.RaiseForm');
Raise::load('core.RaiseFieldFeedback');
Raise::load('core.RaisePostbackFeedback');

/**
 * RaisePostback abstract class
 * A controller that handles postbacks
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @abstract
 * @package Raise.Core.Postback
 * @since 1.2
 */
abstract class RaisePostback extends RaiseObject {

    /**
     * The session key to keep track of the validity of the postback controllers
     */
    const SESSION_KEY = '_?:RaisePostback';

    /**
     * The RaiseApplication that started this RaisePostback
     * @var RaiseApplication
     */
    private $application;

    /**
     * Collection of parameters passed into the postback controller
     * @var RaiseCollection
     */
    private $params;

    /**
     * How long the validation is allowed to live
     * @var RaiseTimeSpan
     */
    protected $timeToLive;

    /**
     * Where will the postback redirect to. This is to follow Post->Redirect->Get recommendations.
     * @link http://en.wikipedia.org/wiki/Post/Redirect/Get
     * @var string|Url
     */
    protected $redirect;

    /**
     * Set whether debugging mode is turned on or not. If debugging mode is on, postback data will be posted on screen. Validation and redirection will then be disabled
     * @var boolean
     */
    protected $debug = false;

    /**
     * The postback feedback handler
     * @var RaisePostbackFeedback
     */
    protected $feedback;

    /**
     * Creates a new RaisePostback Control
     * @param RaiseApplication $app Set the application of this postback controller
     */
    public function __construct($app) {
        $this->timeToLive = new RaiseTimeSpan(3600);
        $this->params = new RaiseCollection();
        $this->application($app);
        $this->feedback = new RaisePostbackFeedback($this->application->session());
    }

    /**
     * Get the collection of parameters passed into this postback controller
     * @return RaiseCollection
     */
    public function params() {
        return $this->params;
    }

    /**
     * Get or set the amount of time that the postback should be allowed to live
     * @param RaiseTimeSpan $t (optional) The new time span
     * @return RaiseTimeSpan
     */
    public function timeToLive($t = null) {
        if (func_num_args() == 1) {
            $this->timeToLive = $t;
        }
        return $this->timeToLive;
    }

    /**
     * Get or set the redirection URL after postback is done. This is to follow PRG recommendations.
     * @param string|Url $r (optional) Sets the new URL to redirect to instead when execution ends.
     * @return string|Url
     */
    public function redirect($r = null) {
        if (func_num_args() == 1) {
            $this->redirect = $r;
        }
        return $this->redirect;
    }

    /**
     * Get or set whether debugging mode is enabled or not.If debugging mode is on, postback data will be posted on screen. Validation and redirection will then be disabled
     * @param boolean $d (optional) Set whether debugging mode is on or off
     * @return boolean
     */
    public function debug($d = null){
        if(func_num_args() == 1){
            $this->debug = $d;
        }
        return $this->debug;
    }

    /**
     * Get or set the RaiseApplication that started this postback controller
     * @param RaiseApplication $app (optional) If set, the new value will be set.
     * @return RaiseApplication
     */
    public function application($app = null) {
        if (func_num_args() == 1) {
            if (!($app instanceof RaiseApplication)) {
                throw new RaiseInvalidArgumentException('RaisePostback::application() expecting $app to be an instance of RaiseApplication, ' . RaiseVariable::typeInfo($app) . ' given instead.');
            }
            $this->application = $app;
        }
        return $this->application;
    }

    /**
     * Raise a postback field exception
     * @param string $field
     * @param string $message
     */
    protected function raiseException($field, $message){
        $feedback = $this->feedback->feedback()->get($field);
        if($feedback){
            $feedback->exceptions()->add(new RaisePostbackException($message));
        }
    }

    /**
     * Check if any exception has been raised by the postback
     * @return boolean
     */
    protected function hasErrors(){
        foreach($this->feedback->feedback() as $f){
            if($f->exceptions()->count() > 0){
                return true;
            }
        }
        return false;
    }

    /**
     * Prepare the RaisePostback for receiving information
     * @param RaiseApplication $app The application to prepare this postback controller for
     * @final
     * @static
     */
    public final static function prepare($app) {
        $app->plugin()->callHook('raise.postback.prepare.start', $this);
        $prepared = $app->session()->get(self::SESSION_KEY);
        if (!RaiseVariable::isType($prepared, 'RaiseCollection')) {
            $prepared = new RaiseCollection();
        }

        $prepared[get_called_class()] = RaiseDateTime::now();
        $app->session()->set(self::SESSION_KEY, $prepared);
        $app->plugin()->callHook('raise.postback.prepare.end', $this);
    }

    /**
     * Validate if the RaisePostback is designated to handle this request
     * and validate certain incoming variables that is needed by the controller
     * @return boolean TRUE if is valid, FALSE otherwise
     */
    public function validate() {
        $this->application->plugin()->callHook('raise.postback.validate.start', $this);
        if($this->debug){
            return true;
        }
        $prepared = $this->application()->session()->get(self::SESSION_KEY);
        if (!RaiseVariable::isType($prepared, 'RaiseCollection')) {
            $prepared = new RaiseCollection();
        }
        $name = get_class($this);
        if ($prepared->keyExists($name)) {
            $expired = $prepared[$name]->add($this->timeToLive)->compareTo(RaiseDateTime::now()) == 1;
            if ($prepared[$name] && !$expired) {
                unset($prepared[$name]);
                $this->application()->session()->set(self::SESSION_KEY, $prepared);

                $validatorOK = true;

                if ($this->application()->request()->post()->keyExists(RaiseForm::VALIDATOR_MAPPER_FIELD)) {
                    $formID = $this->application()->request()->post(RaiseForm::VALIDATOR_MAPPER_FIELD);
                    $allFormMappings = new RaiseCollection($this->application()->session()->get(RaiseForm::SESSION_KEY));
                    if ($allFormMappings->keyExists($formID)) {
                        $mappings = $allFormMappings->get($formID);
                        foreach ($mappings as $k => $m) {
                            if (class_exists($m)) {
                                $validator = new $m();
                                if ($validator instanceof RaiseValidator) {
                                    $validator->validate($this->applicaiton()->request()->post($k));

                                    if ($validator->isValid()) {
                                        continue;
                                    }
                                }
                            }
                            $validatorOK = false;
                            break;
                        }
                        $allFormMappings->remove($formID);
                    } else {
                        $validatorOK = false;
                    }
                }

                if ($validatorOK) {
                    $this->application->plugin()->callHook('raise.postback.validate.end.success', $this);
                    return true;
                }
            }
        }
        $this->application->plugin()->callHook('raise.postback.validate.end.fail', $this);
        return false;
    }

    /**
     * Run the postback controller on a specific action
     * @param string $action Name of the method to execute
     */
    public function run($action) {
        $this->application->plugin()->callHook('raise.postback.run.start', $this);
        $name = get_class($this);
        if (!in_array($action, get_class_methods($name))) {
            throw new RaiseInvalidRequestException('Action \'' . $action . '\' is not found in postback controller \'' . $name . '\'. Please make sure you have a method \'' . $name . '\'->\'' . $action . '\'() defined in the controller.');
        }

        $this->application->accessControl()->checkPostbackAccess($this, $action);

        foreach($this->params() as $k => $v){
            $this->feedback->feedback()->add($k, new RaiseFieldFeedback($k, $v));
        }

        $this->$action();
        $this->feedback->save();
        $this->application->plugin()->callHook('raise.postback.run.end', $this);
    }

    public function __destruct() {
        parent::__destruct();
        if($this->debug){
            echo '<h1>Controller</h1>';
            echo '<pre>'; var_dump(get_class($this)); echo '</pre>';
            echo '<h1>POST Data</h1>';
            echo '<pre>'; var_dump($_POST); echo '</pre>';
            echo '<h1>SESSION Data</h1>';
            echo '<pre>'; var_dump($_SESSION); echo '</pre>';
            echo '<h1>Parameters</h1>';
            echo '<pre>'; var_dump($this->params); echo '</pre>';
        } elseif ($this->redirect) {
            $this->application->plugin()->callHook('raise.postback.run.end.redirect', $this);
            $this->application()->response()->redirect($this->redirect);
        } else{
            $this->application()->response()->output();
        }
    }

}
